Foundations for non-linear solver and polymorphic application - #15287
Merged
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1317
Fixes #5738
Fixes #12919
(also fixes a
FIXcomment that is more than 10 years old according to git blame)Note: although this PR fixes most typical use-cases for type inference against generic functions, it is intentionally incomplete, and it is made in a way to limit implications to small scope.
This PR has essentially three components (better infer, better solve, better apply - all three are needed for this MVP to work):
constraints.py: if the actual function is generic, we unify it with template before inferring constraints. This prevents leaking generic type variables of actual in the solutions (which makes no sense), but also introduces new kind of constraintsT <: F[S], where type variables we solve for appear in target type. These are much harder to solve, but also it is a great opportunity to play with them to prepare for single bin inference (if we will switch to it in some form later). Note unifying is not the best solution, but a good first approximation (see below on what is the best solution).solve.py. The full algorithm is outlined in the docstring forsolve_non_linear(). It looks like it should be able to solve arbitrary constraints that don't (indirectly) contain "F-bounded" things likeT <: list[T]. Very short the idea is to compute transitive closure, then organize constraints by topologically sorted SCCs.checkexpr.py. In cases where solver identifies there are free variables (e.g. we have just one constraintS <: list[T], soTis free, and solution forSislist[T]) it will apply the solutions while creating new generic functions. For example, if we have a functiondef [S, T] (fn: Callable[[S], T]) -> Callable[[S], T]applied to a functiondef [U] (x: U) -> U, this will result indef [T] (T) -> Tas the return.I want to put here some thoughts on the last ingredient, since it may be mysterious, but now it seems to me it is actually a very well defined procedure. The key point here is thinking about generic functions as about infinite intersections or infinite overloads. Now reducing these infinite overloads/intersections to finite ones it is easy to understand what is actually going on. For example, imagine we live in a world with just two types
intandstr. Now we have two functions:the first one can be seen as overload over
and second as an overload over
Now what happens when I apply
dec(id)? We need to choose an overload that matches the argument (this is what we call type inference), but here is a trick, in this case two overloads ofdecmatch the argument type. So (and btw I think we are missing this for real overloads) we construct a new overload that returns intersection of matching overloads# 1and# 4. So if we generalize this intuition to the general case, the inference is selection of an (infinite) parametrized subset among the bigger parameterized set of intersecting types. The only question is whether resulting infinite intersection is representable in our type system. For exampleforall T. dict[T, T]can make sense but is not representable, whileforall T. (T) -> Tis a well defined type. And finally, there is a very easy way to find whether a type is representable or not, we are already doing this during semantic analyzis. I use the same logic (that I used to view as ad-hoc because of lack of good syntax for callables) to bind type variables in the inferred type.OK, so here is the list of missing features, and some comments on them:
Sequence[T] <: S <: Sequence[U] => T <: UParamSpec(and probablyTypeVarTuple). Current support for applying callables withParamSpecto generics is hacky, and kind of dead-end. Although(Callable[P, T]) -> Callable[P, List[T]]works when applied toid, even a slight variation like(Callable[P, List[T]]) -> Callable[P, T]fails. I think it needs to be re-worked in the framework I propose (the tests I added are just to be sure I don't break existing code)<nothing>/object).LRUCache[[x: T], T]. Btw note that I apply force expansion to type aliases and callback protocols. Since I can't transform e.g.A = Callable[[T], T]into a generic callable without getting proper type.T <: List[int], T <: List[S].I am planning to address at least majority of the above items, but I think we should move slowly, since in my experience type inference is really fragile topic with hard to predict long reaching consequences. Please play with this PR if you want to and have time, and please suggest tests to add.